package org.cjprods.rsms;

import java.io.*;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.sql.*;
import java.util.Base64;
import java.util.Collections;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.json.*;
import org.mindrot.jbcrypt.BCrypt;

public class utils
{
    public static class failure extends RuntimeException
    {
        public failure(String msg) { super(msg); }
        public String message() { return getMessage(); }
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // json related
    //
    static public String string(JsonObject jobj)
    {
        StringWriter sw = new StringWriter();
        try (JsonWriter jw = Json.createWriter(sw)) {
            jw.writeObject(jobj);
        }
        return sw.toString();
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // base64 related
    //
    static public String encode_base64(byte[] x)
    {
        String y = Base64.getUrlEncoder().encodeToString(x);
        int npad = 0;
        while (y.endsWith("=")) {
            npad++;
            y = y.substring(0, y.length()-1);
        }
         return y + npad;
    }

    static public String encode_base64(String x)
    {
        try {
            return encode_base64(x.getBytes("UTF-8"));
        }
        catch (UnsupportedEncodingException ex) {
            Logger.getLogger(utils.class.getName()).log(Level.SEVERE, null, ex);
            throw new failure("internal error");
        }
    }

    static public byte[] decode_base64(String x)
    {
        String y = x.substring(0, x.length()-1);

        int npad;
        try { npad = Integer.parseInt(x.substring(x.length()-1)); }
        catch (NumberFormatException ex) {
            Logger.getLogger(utils.class.getName()).log(Level.SEVERE, null, ex);
            throw new failure("bad base64");
        }

        switch (npad) {
            case 3: y += "=";   // fall through intentionally
            case 2: y += "=";   // fall through intentionally
            case 1: y += "=";   // fall through intentionally
            case 0: break;
            default: throw new failure("bad base64");
        }

        return Base64.getUrlDecoder().decode(y);
    }

    static public String decode_base64s(String x)
    {
        try {
            return new String(decode_base64(x), "UTF-8");
        }
        catch (UnsupportedEncodingException ex) {
            Logger.getLogger(utils.class.getName()).log(Level.SEVERE, null, ex);
            throw new failure("internal error");
        }
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // authentication related
    //
    static public String make_credential(String password)
    {
        return BCrypt.hashpw(password, BCrypt.gensalt());
    }

    static public boolean check_password(String credential, String password)
    {
        return BCrypt.checkpw(password, credential);
    }

    static private byte[] hmac_md5(byte[] data, byte[] key)
    {
        try {
            Mac mac = Mac.getInstance("HmacMD5");
            mac.init(new SecretKeySpec(key, "HmacMD5"));
            return mac.doFinal(data);
        }
        catch (NoSuchAlgorithmException | InvalidKeyException ex) {
            Logger.getLogger(utils.class.getName()).log(Level.SEVERE, null, ex);
            throw new failure("internal error");
        }
    }

    static public byte[] hmac(String x)
    {
        try {
            return hmac_md5(x.getBytes("UTF-8"),
                            config.token_secret().getBytes("UTF-8"));
        }
        catch (UnsupportedEncodingException ex) {
            Logger.getLogger(utils.class.getName()).log(Level.SEVERE, null, ex);
            throw new failure("internal error");
        }
    }

    static public String make_token(String seq, String role)
    {
        String raw_token = string(
                Json.createObjectBuilder()
                        .add("seq", seq)
                        .add("role", role)
                        .build());
        String raw_token64 = encode_base64(raw_token);

        byte[] auth = hmac(raw_token64);
        String auth64 = encode_base64(auth);

        return raw_token64 + "." + auth64;
    }

    /**
    @return raw_token64 if token is valid;
            throws if invalid
    */
    static public String check_token(String token)
    {
        String[] shards = token.split("\\.");
        if (shards.length != 2) throw new failure("denied");

        byte[] auth = hmac(shards[0]);
        String auth64 = encode_base64(auth);
        if (!auth64.equals(shards[1])) throw new failure("denied");

        return shards[0];
    }

    /**
    @return [seq, role]
    */
    static public String[] unpack_token(String token)
    {
        String raw_token64 = check_token(token);
        String raw_token = decode_base64s(raw_token64);

        StringReader sr = new StringReader(raw_token);
        JsonReader jr = Json.createReader(sr);
        JsonObject jobj = jr.readObject();

        return new String[] {
            jobj.getString("seq"),
            jobj.getString("role"),
        };
    }


    ////////////////////////////////////////////////////////////////////////////
    //
    // database related
    //
    static private Connection database(String host, String db, String username, String password)
    {
        try {
            Class.forName("org.postgresql.Driver");
            return DriverManager.getConnection("jdbc:postgresql://" + host + "/" + db, username, password);
        }
        catch (ClassNotFoundException | SQLException ex) {
            Logger.getLogger(utils.class.getName()).log(Level.SEVERE, null, ex);
            throw new failure("bad database: " + ex.getMessage());
        }
    }

    static public Connection database()
    {
        return database(config.database_host(),
                        config.database_name(),
                        config.database_username(),
                        config.database_password());
    }

    static public PreparedStatement prepare(
            Connection conn, String name, String... args) throws SQLException
    {
        String params = String.join(",", Collections.nCopies(args.length, "?"));
        String sql = String.format("SELECT * FROM %s(%s)", name, params);

        PreparedStatement ps = conn.prepareStatement(sql);
        int i = 1;
        for (String arg: args) ps.setString(i++, arg);
        return ps;
    }

    /**
    @param conn
    @param seq NonNull
    @return NonNull
    */
    static public String user_from_seq(Connection conn, String seq)
    {
        try (PreparedStatement ps = prepare(conn, "user_from_seq", seq);
                ResultSet rs = ps.executeQuery()) {
            rs.next();
            return rs.getString(1);
        }
        catch (SQLException ex) {
            Logger.getLogger(utils.class.getName()).log(Level.SEVERE, null, ex);
            if ("RSMS1".equals(ex.getSQLState()))   // no such seq
                throw new failure("denied");
            throw new failure("bad database: " + ex.getMessage());
        }
    }
}

